3

调用分布式服务接口时经常会遇到这样的问题:接口方提供多个 IP 供用户调用,只要有一个返回成功就算成功。

对于这样的问题,一个比较简单的方案是依次调用各个接口,如果前一个接口未成功返回再调用第二个接口。这样做的好处是对于服务器资源消耗比较小,但对于用户来说效率非常低下。设想调用第一个接口经过 20 秒超时出错才调用第二个接口,如果第二个接口又是 20 秒超时,用户就已经等待了 40 秒。用户的等待时间按线性增长,这样的结果时不可接受的。

好的解决方案是同时并发调用所有接口,有些同学可能会想到 Promise.race。但是请注意:Promise.race 不是等到有一个 Promise 被 resolve 时返回,而是只要有一个 Promise 被 fulfill 时就会返回,不论这个 Promise 是 resolve 还是 reject。

我就遇到过类似问题,当然也被 Promise.race 坑害了一回。上谷歌搜到了老外这篇博文:Promise me you won’t use Promise.race。博文中详细探讨了这个问题,并用 Promise.race 实现了一个解决方案:

Promise.properRace = function properRace(promises) {
  if (promises.length < 1) {
    return Promise.reject('Can\'t start a race without promises!');
  }

  // There is no way to know which promise is rejected.
  // So we map it to a new promise to return the index when it fails
  const indexPromises = promises.map((p, index) => p.catch(e => {
    console.debug('Promise rejected in `properRace`: ' + e);
    return Promise.reject(index);
  }));

  return Promise.race(indexPromises).catch(index => {
    // The promise has rejected, remove it from the list of promises and just continue the race.
    promises.splice(index, 1)[0].catch(() => { /* eat this */ });
    return promises.length ? properRace(promises) : Promise.reject('All promises rejected');
  });
};

虽然略显复杂,但方法确实很巧妙,我初期就使用了这种方法。最近回过头来突然想到:properRace 的需求是只要有一个被 resolve 返回的 Promise 就被 resolve;JavaScript 虽然没有提供这样的函数,但是提供了另外一个很相似的函数:只要有一个被 reject 返回的 Promise 就被 reject。

这个函数我们很常用,就是:Promise.all

想到这一层,那么用它来实现 properRace 就很简单了:把传入的 Promise 数组状态正反对调就好了。结果如下:

Promise.properRace = function properRace(promises) {
  const resolve = Promise.resolve.bind(Promise);
  const reject = Promise.reject.bind(Promise);
  return Promise.all(promises.map(x => x.then(reject, resolve)))
    .then(reject, resolve);
}

2019年7月更新

此方法已经被标准化为 Promise.any


CarterLi
1.3k 声望102 粉丝